<template>
  <div class="position-relative">
    <template v-if="isDataLoading">
      <slot name="loading"> Loading... </slot>
    </template>
    <template v-else>
      <template v-if="isDataComputing">
        <slot name="computing">
          <div class="position-absolute w-100 h-100 text-center" style="z-index: 1">
            <div class="position-sticky d-inline-block mt-5 p-3" style="top: 0">Loading table values...</div>
          </div>
        </slot>
      </template>

      <div v-if="data.length === 0 || (rows.length === 0 && cols.length === 0)" class="alert alert-warning" role="alert">
        {{ noDataWarningText }}
      </div>

      <div v-else-if="cols.length && rows.length" class="table-responsive">
        <table class="table table-bordered" :aria-busy="isDataLoading || isDataComputing">
          <!-- Table header -->
          <thead>
            <template v-for="({ colField, cols }, colFieldIndex) in tableHeader.colHeaderFields">
              <!-- Multiple slots -->
              <template v-if="colField.headerSlotNames">
                <tr v-for="(headerSlotName, headerSlotNameIndex) in colField.headerSlotNames" :key="`${colFieldIndex}-${headerSlotNameIndex}`">
                  <!-- Top left dead zone -->
                  <th v-if="colFieldIndex === 0 && rowHeaderSize > 0 && headerSlotNameIndex === 0" :colspan="rowHeaderSize" :rowspan="colHeaderSize"></th>
                  <!-- Column headers -->
                  <th v-for="(col, colIndex) in cols" :key="`${colFieldIndex}-${headerSlotNameIndex}-${colIndex}`" :colspan="col.colspan">
                    <slot :name="headerSlotName" :value="col.value">
                      Missing slot <code>{{ headerSlotName }}</code>
                    </slot>
                  </th>
                  <!-- Top right dead zone -->
                  <th v-if="colFieldIndex === 0 && rowFooterSize > 0 && headerSlotNameIndex === 0" :colspan="rowFooterSize" :rowspan="colHeaderSize"></th>
                </tr>
              </template>
              <!-- Single slot/no slot -->
              <tr v-else :key="colFieldIndex">
                <!-- Top left dead zone -->
                <th v-if="colFieldIndex === 0 && rowHeaderSize > 0" :colspan="rowHeaderSize" :rowspan="colHeaderSize"></th>
                <!-- Column headers -->
                <th v-for="(col, colIndex) in cols" :key="`${colFieldIndex}-${colIndex}`" :colspan="col.colspan">
                  <slot v-if="colField.headerSlotName" :name="colField.headerSlotName" :value="col.value">
                    Missing slot <code>{{ colField.headerSlotName }}</code>
                  </slot>
                  <template v-else>
                    {{ col.value }}
                  </template>
                </th>
                <!-- Top right dead zone -->
                <th v-if="colFieldIndex === 0 && rowFooterSize > 0" :colspan="rowFooterSize" :rowspan="colHeaderSize"></th>
              </tr>
            </template>
          </thead>

          <!-- Table body -->
          <tbody>
            <tr v-for="({ row, rowHeaderFields, rowFooterFields }, rowIndex) in tableRows" :key="rowIndex">
              <!-- Row headers -->
              <template v-for="(rowField, rowFieldIndex) in rowHeaderFields">
                <!-- Multiple slots -->
                <template v-if="rowField.headerSlotNames">
                  <th v-for="(headerSlotName, headerSlotNameIndex) in rowField.headerSlotNames" :key="`header-${rowIndex}-${rowFieldIndex}-${headerSlotNameIndex}`" :rowspan="rowField.rowspan">
                    <slot :name="headerSlotName" :value="rowField.value">
                      Missing slot <code>{{ headerSlotName }}</code>
                    </slot>
                  </th>
                </template>
                <!-- Single slot/no slot -->
                <th v-else :key="`header-${rowIndex}-${rowFieldIndex}`" :rowspan="rowField.rowspan">
                  <slot v-if="rowField.headerSlotName" :name="rowField.headerSlotName" :value="rowField.value">
                    Missing slot <code>{{ rowField.headerSlotName }}</code>
                  </slot>
                  <template v-else>
                    {{ rowField.value }}
                  </template>
                </th>
              </template>

              <!-- Values -->
              <td v-for="(col, colIndex) in sortedCols" :key="`value-${rowIndex}-${colIndex}`" class="text-right">
                <!-- <slot v-if="$scopedSlots.value" name="value" :value="value(row, col)" :labels="labels(row, col)" />
                <template v-else>{{ value(row, col) }}</template> -->
                <template v-if="$slots.value">
                  <slot name="value" :value="value(row, col)" :labels="labels(row, col)" />
                </template>
                <template v-else>
                  {{ value(row, col) }}
                </template>
              </td>

              <!-- Row footers (if slots are provided) -->
              <template v-for="(rowField, rowFieldIndex) in rowFooterFields">
                <!-- Multiple slots -->
                <template v-if="rowField.footerSlotNames">
                  <th v-for="(footerSlotName, footerSlotNameIndex) in rowField.footerSlotNames" :key="`footer-${rowIndex}-${rowFieldIndex}-${footerSlotNameIndex}`" :rowspan="rowField.rowspan">
                    <slot :name="footerSlotName" :value="rowField.value">
                      Missing slot <code>{{ footerSlotName }}</code>
                    </slot>
                  </th>
                </template>
                <!-- Single slot/no slot -->
                <th v-else :key="`footer-${rowIndex}-${rowFieldIndex}`" :rowspan="rowField.rowspan">
                  <slot v-if="rowField.footerSlotName" :name="rowField.footerSlotName" :value="rowField.value">
                    Missing slot <code>{{ rowField.footerSlotName }}</code>
                  </slot>
                  <template v-else>
                    {{ rowField.value }}
                  </template>
                </th>
              </template>
            </tr>
          </tbody>

          <!-- Table footer -->
          <tfoot>
            <template v-for="({ colField, cols }, colFieldIndex) in tableFooter.colFooterFields">
              <!-- Multiple slots -->
              <template v-if="colField.footerSlotNames">
                <tr v-for="(footerSlotName, footerSlotNameIndex) in colField.footerSlotNames" :key="`${colFieldIndex}-${footerSlotNameIndex}`">
                  <!-- Bottom left dead zone -->
                  <th v-if="colFieldIndex === 0 && rowHeaderSize > 0 && footerSlotNameIndex === 0" :colspan="rowHeaderSize" :rowspan="colFooterSize"></th>
                  <!-- Column footers -->
                  <th v-for="(col, colIndex) in sortedCols" :key="`${colFieldIndex}-${footerSlotNameIndex}-${colIndex}`" :colspan="col.colspan">
                    <slot :name="footerSlotName" :value="col.value">
                      Missing slot <code>{{ footerSlotName }}</code>
                    </slot>
                  </th>
                  <!-- Bottom right dead zone -->
                  <th v-if="colFieldIndex === 0 && rowFooterSize > 0 && footerSlotNameIndex === 0" :colspan="rowFooterSize" :rowspan="colFooterSize"></th>
                </tr>
              </template>
              <!-- Single slot/no slot -->
              <tr v-else :key="colFieldIndex">
                <!-- Bottom left dead zone -->
                <th v-if="colFieldIndex === 0 && rowHeaderSize > 0" :colspan="rowHeaderSize" :rowspan="colFooterSize"></th>
                <!-- Column footers -->
                <th v-for="(col, colIndex) in cols" :key="`${colFieldIndex}-${colIndex}`" :colspan="col.colspan">
                  <slot v-if="colField.footerSlotName" :name="colField.footerSlotName" :value="col.value">
                    Missing slot <code>{{ colField.footerSlotName }}</code>
                  </slot>
                  <template v-else>
                    {{ col.value }}
                  </template>
                </th>
                <!-- Bottom right dead zone -->
                <th v-if="colFieldIndex === 0 && rowFooterSize > 0" :colspan="rowFooterSize" :rowspan="colFooterSize"></th>
              </tr>
            </template>
          </tfoot>
        </table>
      </div>
    </template>
  </div>
</template>

<script>
import ManyKeysMap from 'many-keys-map'
import { firstBy } from 'thenby'
import naturalSort from 'javascript-natural-sort'
import cloneDeep from 'lodash-es/cloneDeep'
import isEqual from 'lodash-es/isEqual'

export default {
  props: {
    data: {
      type: Array,
      default: () => []
    },
    rowFields: {
      type: Array,
      default: () => []
    },
    colFields: {
      type: Array,
      default: () => []
    },
    reducer: {
      type: Function,
      default: (sum) => sum + 1
    },
    reducerInitialValue: {
      default: 0
    },
    noDataWarningText: {
      type: String,
      default: 'No data to display.'
    },
    isDataLoading: {
      type: Boolean,
      default: false
    }
  },
  data: function () {
    return {
      valuesMap: null,
      labelsMap: null,
      rows: [],
      cols: [],
      // Note: we don't use directly rowFields/colFields props to trigger table render when `updateValues` has finished
      internalRowFields: [],
      internalColFields: [],
      isDataComputing: false,
      computingInterval: null
    }
  },
  computed: {
    // Sort rows/cols using a composed function built with thenBy.js
    sortedRows: function () {
      let composedSortFunction
      this.internalRowFields.forEach((rowField, rowFieldIndex) => {
        if (rowFieldIndex === 0) {
          composedSortFunction = firstBy(0, { cmp: rowField.sort || naturalSort })
        } else {
          composedSortFunction = composedSortFunction.thenBy(rowFieldIndex, { cmp: rowField.sort || naturalSort })
        }
      })

      return [...this.rows].sort(composedSortFunction)
    },
    sortedCols: function () {
      let composedSortFunction
      this.internalColFields.forEach((colField, colFieldIndex) => {
        if (colFieldIndex === 0) {
          composedSortFunction = firstBy(0, { cmp: colField.sort || naturalSort })
        } else {
          composedSortFunction = composedSortFunction.thenBy(colFieldIndex, { cmp: colField.sort || naturalSort })
        }
      })

      return [...this.cols].sort(composedSortFunction)
    },
    // Property to watch specific field changes
    fields: function () {
      return [
        [this.colFields, this.rowFields],
        [this.colFields.map((field) => field.headerSlotNames), this.rowFields.map((field) => field.headerSlotNames)],
        [this.colFields.map((field) => field.valuesFiltered), this.rowFields.map((field) => field.valuesFiltered)]
      ]
    },
    // Number of row header columns
    rowHeaderSize: function () {
      return this.internalRowFields.reduce((acc, rowField) => {
        if (rowField.showHeader === undefined || rowField.showHeader) {
          if (rowField.headerSlotNames) return acc + rowField.headerSlotNames.length
          else return acc + 1
        } else return acc
      }, 0)
    },
    // Number of row footer columns
    rowFooterSize: function () {
      return this.internalRowFields.reduce((acc, rowField) => {
        if (rowField.showFooter) {
          if (rowField.footerSlotNames) return acc + rowField.footerSlotNames.length
          else return acc + 1
        } else return acc
      }, 0)
    },
    // Number of col header rows
    colHeaderSize: function () {
      return this.internalColFields.reduce((acc, colField) => {
        if (colField.showHeader === undefined || colField.showHeader) {
          if (colField.headerSlotNames) return acc + colField.headerSlotNames.length
          else return acc + 1
        } else return acc
      }, 0)
    },
    // Number of col footer rows
    colFooterSize: function () {
      return this.internalColFields.reduce((acc, colField) => {
        if (colField.showFooter) {
          if (colField.footerSlotNames) return acc + colField.footerSlotNames.length
          else return acc + 1
        } else return acc
      }, 0)
    },
    // Table header
    tableHeader: function () {
      const colHeaderFields = this.internalColFields
        .map((colField, colFieldIndex) => {
          return {
            colField,
            cols: this.sortedCols
              .map((col, colIndex) => ({
                value: col[colFieldIndex],
                colspan: this.spanSize(this.sortedCols, colIndex, colFieldIndex)
              }))
              .filter((colField) => colField.colspan !== 0)
          }
        })
        .filter(({ colField }) => colField.showHeader === undefined || colField.showHeader)

      return {
        colHeaderFields
      }
    },
    // Table footer
    tableFooter: function () {
      const colFooterFields = this.internalColFields
        .map((colField, colFieldIndex) => {
          return {
            colField,
            cols: this.sortedCols
              .map((col, colIndex) => ({
                value: col[colFieldIndex],
                colspan: this.spanSize(this.sortedCols, colIndex, colFieldIndex)
              }))
              .filter((colField) => colField.colspan !== 0)
          }
        })
        .filter(({ colField }) => colField.showFooter)
        .reverse()

      return {
        colFooterFields
      }
    },
    // Table rows with row headers/footers with rowspan
    tableRows: function () {
      return this.sortedRows.map((row, rowIndex) => {
        const rowHeaderFields = this.internalRowFields
          .map((rowField, rowFieldIndex) => ({
            ...rowField,
            value: row[rowFieldIndex],
            rowspan: this.spanSize(this.sortedRows, rowIndex, rowFieldIndex)
          }))
          .filter((rowField) => (rowField.showHeader === undefined || rowField.showHeader) && rowField.rowspan !== 0)

        const rowFooterFields = this.internalRowFields
          .map((rowField, rowFieldIndex) => ({
            ...rowField,
            value: row[rowFieldIndex],
            rowspan: this.spanSize(this.sortedRows, rowIndex, rowFieldIndex)
          }))
          .filter((rowField) => rowField.showFooter && rowField.rowspan !== 0)
          .reverse()

        return {
          row,
          rowHeaderFields,
          rowFooterFields
        }
      })
    }
  },
  methods: {
    // Get value from valuesMap
    value: function (row, col) {
      return this.valuesMap.get([...row, ...col]) || this.reducerInitialValue
    },
    // Get labels for a cell
    labels: function (row, col) {
      return this.labelsMap.get([...row, ...col]) || []
    },
    // Get colspan/rowspan size
    spanSize: function (values, valueIndex, fieldIndex) {
      // If left value === current value
      // and top value === 0 (= still in the same top bracket)
      // The left td will take care of the display
      if (valueIndex > 0 && values[valueIndex - 1][fieldIndex] === values[valueIndex][fieldIndex] && (fieldIndex === 0 || this.spanSize(values, valueIndex, fieldIndex - 1) === 0)) {
        return 0
      }

      // Otherwise, count entries on the right with the same value
      // But stop if the top value !== 0 (= the top bracket has changed)
      let size = 1
      let i = valueIndex
      while (i + 1 < values.length && values[i + 1][fieldIndex] === values[i][fieldIndex] && (fieldIndex === 0 || (i + 1 < values.length && this.spanSize(values, i + 1, fieldIndex - 1) === 0))) {
        i++
        size++
      }

      return size
    },
    // Update rows/cols/valuesMap (optional)
    // @param {boolean} updateValuesMap
    updateValues: function (updateValuesMap = true) {
      this.isDataComputing = true

      // Start a task to avoid blocking the page
      clearInterval(this.computingInterval)
      this.computingInterval = setTimeout(() => {
        const rows = []
        const cols = []
        const valuesMap = new ManyKeysMap()
        const labelsMap = new ManyKeysMap()

        const fields = [...this.rowFields, ...this.colFields]

        this.data.forEach((item) => {
          const rowKey = []
          const colKey = []
          let filtered = false

          // Check if item passes fields value filters
          for (let field of fields) {
            if (field.valuesFiltered && !field.valuesFiltered.has(field.getter(item))) {
              filtered = true
              break
            }
          }

          // Build item rowKey/colKey if necessary
          if (!filtered || updateValuesMap) {
            this.rowFields.forEach((field) => {
              rowKey.push(field.getter(item))
            })

            this.colFields.forEach((field) => {
              colKey.push(field.getter(item))
            })
          }

          // Update rows/cols
          if (!filtered) {
            if (
              !rows.some((row) => {
                return this.rowFields.every((rowField, index) => row[index] === rowKey[index])
              })
            ) {
              rows.push(rowKey)
            }

            if (
              !cols.some((col) => {
                return this.colFields.every((colField, index) => col[index] === colKey[index])
              })
            ) {
              cols.push(colKey)
            }
          }

          // Update valuesMap
          if (updateValuesMap) {
            const key = [...rowKey, ...colKey]
            const previousValue = valuesMap.get(key) || cloneDeep(this.reducerInitialValue)

            valuesMap.set(key, this.reducer(previousValue, item))
          }
        })

        // Update labelsMap
        rows.forEach((row) => {
          cols.forEach((col) => {
            const key = [...row, ...col]
            const labels = fields.map((field, fieldIndex) => ({
              field,
              value: key[fieldIndex]
            }))
            labelsMap.set(key, labels)
          })
        })

        this.rows = rows
        this.cols = cols
        if (updateValuesMap) this.valuesMap = valuesMap
        this.labelsMap = labelsMap

        this.isDataComputing = false
        this.updateInternalFields()
      }, 0)
    },
    // Update internal fields only
    updateInternalFields: function () {
      this.internalRowFields = cloneDeep(this.rowFields)
      this.internalColFields = cloneDeep(this.colFields)
    }
  },
  watch: {
    // Check if fields changed
    fields: function (val, oldVal) {
      if (!isEqual(val[0], oldVal[0])) {
        // Fields were moved
        this.updateValues()
      } else if (!isEqual(val[1], oldVal[1])) {
        // Field header value filters changed
        this.updateInternalFields()
      } else if (!isEqual(val[2], oldVal[2])) {
        // Field header slot names changed
        this.updateValues(false)
      }
    },
    data: function () {
      this.updateValues()
    }
  },
  created: function () {
    this.updateValues()
    console.log('-----created------')
  }
}
</script>

<style scoped>
td {
  min-width: 100px;
}
/* 基础表格样式 */
.table {
  width: 100%;
  margin-bottom: 1rem;
  color: #212529;
  border-collapse: collapse;
}

.table th,
.table td {
  padding: 0.75rem;
  vertical-align: top;
  border-top: 1px solid #dee2e6;
}

.table thead th {
  vertical-align: bottom;
  border-bottom: 2px solid #dee2e6;
}

.table tbody + tbody {
  border-top: 2px solid #dee2e6;
}

/* 表格条纹样式 */
.table-striped tbody tr:nth-of-type(odd) {
  background-color: rgba(0, 0, 0, 0.05);
}

/* 表格 hover 样式 */
.table-hover tbody tr:hover {
  color: #212529;
  background-color: rgba(0, 0, 0, 0.075);
}

/* 表格边框样式 */
.table-bordered {
  border: 1px solid #dee2e6;
}

.table-bordered th,
.table-bordered td {
  border: 1px solid #dee2e6;
  text-wrap: nowrap;
}

.table-bordered thead th,
.table-bordered thead td {
  border-bottom-width: 2px;
}

/* 响应式表格样式 */
.table-responsive {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
</style>
